package com.xjm_cloud_admin.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.common.hash.Hashing;
import com.xjm_cloud_admin.constant.CacheConstants;
import com.xjm_cloud_admin.constant.Constants;
import com.xjm_cloud_admin.constant.HttpStatus;
import com.xjm_cloud_admin.domain.XjmFile;
import com.xjm_cloud_admin.domain.XjmShare;
import com.xjm_cloud_admin.domain.XjmSharingFile;
import com.xjm_cloud_admin.domain.XjmUser;
import com.xjm_cloud_admin.domain.vo.req.*;
import com.xjm_cloud_admin.domain.vo.resp.FileListRespVo;
import com.xjm_cloud_admin.enums.CommonCodeEnum;
import com.xjm_cloud_admin.enums.FileStatusEnum;
import com.xjm_cloud_admin.enums.FileTypeEnum;
import com.xjm_cloud_admin.mapper.XjmFileMapper;
import com.xjm_cloud_admin.redis.RedisCache;
import com.xjm_cloud_admin.security.pojo.LoginUser;
import com.xjm_cloud_admin.security.service.ShareTokenService;
import com.xjm_cloud_admin.security.service.TokenService;
import com.xjm_cloud_admin.service.*;
import com.xjm_cloud_admin.util.UUID;
import com.xjm_cloud_admin.util.*;
import lombok.extern.slf4j.Slf4j;
import net.coobird.thumbnailator.ThumbnailParameter;
import net.coobird.thumbnailator.Thumbnails;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * 文件表(XjmFile)表服务实现类
 *
 * @author 浪漫写尽歌谱
 * @since 2024-04-24 13:16:57
 */
@Service("xjmFileService")
@Slf4j
public class XjmFileServiceImpl extends ServiceImpl<XjmFileMapper, XjmFile> implements XjmFileService, BaseFileService {

    @Value("${sxg.profile}")
    private String filePath;

    @Value("${sxg.filePath}")
    private String mPath;

    @Value("${sxg.ip}")
    private String ip;

    @Value("${server.port}")
    private String port;

    @Resource
    private ThreadPoolTaskExecutor taskExecutor;

    @Resource
    private RedisCache redisCache;

    @Resource
    private XjmFileSharingService xjmFileSharingService;

    @Resource
    private XjmShareService xjmShareService;

    @Resource
    private ShareTokenService shareTokenService;

    @Resource
    private TokenService tokenService;

    @Resource
    private XjmUserService xjmUserService;

    /**
     * 查询文件列表
     *
     * @param vo 查询信息
     * @return 文件列表
     */
    @Override
    public R tree(FileListReqVo vo) {
        // 获取用户id
        Long userId = SecurityUtils.getUserId();
        // 查询文件列表
        List<XjmFile> fileList = baseMapper.selectList(new QueryWrapper<XjmFile>().lambda().eq(XjmFile::getUserId, userId).in(XjmFile::getStatus, FileStatusEnum.NORMAL.getCode(), FileStatusEnum.TRANSCODING.getCode(), FileStatusEnum.TRANSCODING_FAILURE.getCode()).eq(XjmFile::getParentId, vo.getParentId()));
        // 先按日期排序在按status为0的在前面
        List<XjmFile> files = fileList.stream().sorted(Comparator.comparing(XjmFile::getCreateTime).reversed()).sorted(Comparator.comparing(XjmFile::getIsFolder)).collect(Collectors.toList());
        // 转换为返回vo
        return R.success(transformTree(files, ip, port));
    }

    public List<FileListRespVo> sharingTree(String parentId) {
        // 查询文件列表
        List<XjmFile> fileList = baseMapper.selectList(new QueryWrapper<XjmFile>().lambda().in(XjmFile::getStatus, FileStatusEnum.NORMAL.getCode(), FileStatusEnum.TRANSCODING.getCode(), FileStatusEnum.TRANSCODING_FAILURE.getCode()).eq(XjmFile::getParentId, parentId));
        // 先按日期排序在按status为0的在前面
        List<XjmFile> files = fileList.stream().sorted(Comparator.comparing(XjmFile::getCreateTime).reversed()).sorted(Comparator.comparing(XjmFile::getIsFolder)).collect(Collectors.toList());

        // 转换为返回vo
        return transformTree(files, ip, port);
    }

    /**
     * 删除文件
     *
     * @param vo 删除文件id
     */
    @Override
    public R delete(DeleteFileReqVo vo) {
        // 获取用户id
        Long userId = SecurityUtils.getUserId();
        String[] ids = vo.getFileIds().split(",");
        List<XjmSharingFile> list = xjmFileSharingService.list(new QueryWrapper<XjmSharingFile>().lambda().select(XjmSharingFile::getSharingId).in(XjmSharingFile::getFileId, Arrays.asList(ids)));
        List<String> sharingIds = list.stream().map(XjmSharingFile::getSharingId).distinct().collect(Collectors.toList());
        if (!sharingIds.isEmpty()) {
            xjmShareService.update(new UpdateWrapper<XjmShare>().lambda().set(XjmShare::getStatus, "1").eq(XjmShare::getUserId, userId).in(XjmShare::getId, sharingIds));
        }
        baseMapper.update(new UpdateWrapper<XjmFile>().lambda().set(XjmFile::getStatus, FileStatusEnum.RECYCLING.getCode()).in(XjmFile::getId, Arrays.asList(ids)).eq(XjmFile::getUserId, SecurityUtils.getUserId()));
        return R.success();
    }

    /**
     * 获取回收站的文件列表
     */
    @Override
    public R deleteList() {
        // 获取用户id
        Long userId = SecurityUtils.getUserId();
        // 查询文件列表
        List<XjmFile> fileList = baseMapper.selectList(new QueryWrapper<XjmFile>().lambda().eq(XjmFile::getUserId, userId).eq(XjmFile::getStatus, FileStatusEnum.RECYCLING.getCode()));
        // 先按日期排序在按status为0的在前面
        List<XjmFile> files = fileList.stream().sorted(Comparator.comparing(XjmFile::getCreateTime).reversed()).collect(Collectors.toList());
        return R.success(files);
    }

    /**
     * 还原文件
     *
     * @param vo 还原文件id
     */
    @Override
    public R restore(DeleteFileReqVo vo) {
        // 获取用户id
        Long userId = SecurityUtils.getUserId();
        String[] ids = vo.getFileIds().split(",");
        baseMapper.update(new UpdateWrapper<XjmFile>().lambda().set(XjmFile::getStatus, FileStatusEnum.NORMAL.getCode()).in(XjmFile::getId, Arrays.asList(ids)).eq(XjmFile::getUserId, userId));
        return R.success();
    }

    /**
     * 搜索文件
     *
     * @param vo 搜索文件信息
     */
    @Override
    public R search(SearchFileReqVo vo) {
        String name = vo.getName();
        // 获取用户id
        Long userId = SecurityUtils.getUserId();
        // 查询文件列表
        List<XjmFile> fileList = baseMapper.selectList(new QueryWrapper<XjmFile>().lambda().eq(XjmFile::getUserId, userId).eq(XjmFile::getStatus, FileStatusEnum.NORMAL.getCode()).like(XjmFile::getName, name));
        return R.success(transformTree(fileList, ip, port));
    }

    /**
     * 下载文件
     *
     * @param response 响应体
     */
    @Override
    public void download(String token, HttpServletResponse response) {
        String info = shareTokenService.verifyByToken(token);
        if (StringUtils.isEmpty(info)) {
            return;
        }
        // 获取用户id
        String[] infoList = info.split("@");
        String userId = null;
        if (infoList.length > 1) {
            userId = infoList[1];
        }
        // 解析文件id
        ArrayList<Long> ids;
        try {
            String[] split = infoList[0].split(",");
            if (!(split.length > 0)) {
                return;
            }
            ids = new ArrayList<>();
            for (String id : split) {
                long parseLong = Long.parseLong(id);
                ids.add(parseLong);
            }
        } catch (Exception e) {
            log.error("文件id转换失败");
            return;
        }

        // 查询文件列表
        LambdaQueryWrapper<XjmFile> queryWrapper = new QueryWrapper<XjmFile>().lambda();
        List<XjmFile> fileList = baseMapper.selectList(queryWrapper
                .eq(XjmFile::getStatus, FileStatusEnum.NORMAL.getCode())
                .in(XjmFile::getId, ids));

        if (userId != null) {
            queryWrapper.eq(XjmFile::getUserId, userId);
        }

        if (fileList.isEmpty()) {
            return;
        }
        ZipOutputStream zos = null;
        HashSet<String> pathList = new HashSet<>();
        // 设置响应头
        try {
            response.reset();
            XjmFile xjmFile = fileList.get(0);
            String name = "";
            if (ids.size() > 1) {
                name = xjmFile.getName() + "等" + ids.size() + "个文件.zip";
            } else {
                if ("0".equals(xjmFile.getIsFolder())) {
                    name = xjmFile.getName() + ".zip";
                } else {
                    name = xjmFile.getName();
                }
            }
            // inline在浏览器中直接显示，不提示用户下载
            // attachment弹出对话框，提示用户进行下载保存本地
            // 默认为inline方式
            response.setCharacterEncoding("utf-8");//设置编码格式为utf-8
            response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + URLEncoder.encode(name, "UTF-8"));
            response.setStatus(200);

            //设置成这样可以不用保存在本地，再输出， 通过response流输出,直接输出到客户端浏览器中。

            if (ids.size() > 1 || "0".equals(xjmFile.getIsFolder())) {
                response.setHeader(HttpHeaders.CONTENT_TYPE, "application/zip");
                zos = new ZipOutputStream(response.getOutputStream());
                zipFile(fileList, zos, userId, "", pathList);
                zos.flush();
                zos.close();
            } else {
                response.setHeader(HttpHeaders.CONTENT_TYPE, "application/octet-stream");
                response.setHeader(HttpHeaders.CONTENT_LENGTH, xjmFile.getSize() + "");
                byte[] buffer = new byte[1024];
                File inputFile = new File(mPath + File.separator + xjmFile.getPath());
                if (inputFile.exists()) {
                    if (!"0".equals(xjmFile.getIsFolder())) {
                        //创建输入流读取文件
                        BufferedInputStream bis = new BufferedInputStream(Files.newInputStream(inputFile.toPath()));
                        //写入文件的方法，同上
                        int size = 0;
                        //设置读取数据缓存大小
                        while ((size = bis.read(buffer)) > 0) {
                            response.getOutputStream().write(buffer, 0, size);
                        }
                        //关闭输入输出流
                        bis.close();
                    }
                }
            }
        } catch (IOException e) {
            log.error("文件下载失败", e);
        } finally {

            if (null != zos) {
                try {
                    //关闭输入输出流
                    zos.flush();
                    zos.close();
                } catch (IOException e) {
                    log.error("文件打包关闭失败", e);
                }
            }

            try {
                response.getOutputStream().flush();
                response.getOutputStream().close();
            } catch (IOException e) {
                log.error("关闭输出流失败", e);
            }
        }

    }

    @Override
    public R createdDownload(DownloadFileReqVo vo) {
        // 解析文件id
        ArrayList<Long> idsList;
        try {
            String[] split = vo.getIds().split(",");
            if (!(split.length > 0)) {
                return R.error(HttpStatus.SHOW_TOAST, "下载失败");
            }
            idsList = new ArrayList<>();
            for (String id : split) {
                long parseLong = Long.parseLong(id);
                idsList.add(parseLong);
            }
        } catch (Exception e) {
            log.error("文件id转换失败");
        }
        Long userId = SecurityUtils.getUserId();
        String token = shareTokenService.createToken(vo.getIds() + "@" + userId);
        return R.success().put("data", token);
    }

    /**
     * 分享链接创建下载链接
     *
     * @param httpServletRequest 下载文件信息
     */
    @Override
    public R sharingCreatedDownload(HttpServletRequest httpServletRequest, String fileId) {
        if (StringUtils.isEmpty(fileId)) {
            return R.error(HttpStatus.SHOW_TOAST, "文件id不能为空");
        }
        String s = shareTokenService.verifyToken(httpServletRequest);
        if (StringUtils.isEmpty(s)) {
            return R.error(HttpStatus.SHOW_TOAST, "链接已失效");
        }
        String token = null;
        try {
            // 解析文件id
            String[] split = fileId.split(",");
            if (!(split.length > 0)) {
                return R.error(HttpStatus.SHOW_TOAST, "下载失败");
            }
            List<String> list = Arrays.asList(split);
            for (String string : list) {
                Long.parseLong(string);
            }
            token = shareTokenService.createToken(fileId);
        } catch (Exception e) {
            log.error("链接创建失败", e);
            return R.error(HttpStatus.SHOW_TOAST, "下载失败");
        }
        return R.success().put("data", token);
    }

    private void zipFile(List<XjmFile> fileList, ZipOutputStream zos, String userId, String folder, HashSet<String> set) {
        //设置读取数据缓存大小
        byte[] buffer = new byte[1024];
        // 判断文件类型并压缩
        try {
            for (XjmFile xjmFile : fileList) {

                File inputFile = new File(mPath + File.separator + xjmFile.getPath());
                if (!"0".equals(xjmFile.getIsFolder())) {


                    if (inputFile.exists()) {

                        //创建输入流读取文件
                        BufferedInputStream bis = new BufferedInputStream(Files.newInputStream(inputFile.toPath()));
                        //将文件写入zip内，即将文件进行打包
                        if (StringUtils.isNotEmpty(folder)) {
                            if (set.contains(folder + "/" + xjmFile.getName())) {
                                continue;
                            }
                            set.add(folder + "/" + xjmFile.getName());
                            zos.putNextEntry(new ZipEntry(folder + "/" + xjmFile.getName()));
                        } else {
                            if (set.contains(xjmFile.getName())) {
                                continue;
                            }
                            set.add(xjmFile.getName());
                            zos.putNextEntry(new ZipEntry(xjmFile.getName()));
                        }
                        //写入文件的方法，同上
                        int size = 0;
                        //设置读取数据缓存大小
                        while ((size = bis.read(buffer)) > 0) {
                            zos.write(buffer, 0, size);
                        }
                        bis.close();
                        zos.closeEntry();
                    }
                } else {

                    // 递归压缩文件夹
                    List<XjmFile> list = baseMapper.selectList(new QueryWrapper<XjmFile>().lambda()
                            .eq(XjmFile::getUserId, userId)
                            .eq(XjmFile::getStatus, FileStatusEnum.NORMAL.getCode())
                            .eq(XjmFile::getParentId, xjmFile.getId()));
                    // 排序文件优先打包
                    list = list.stream().sorted(Comparator.comparing(XjmFile::getIsFolder).reversed()).collect(Collectors.toList());
                    if (StringUtils.isNotEmpty(folder)) {
                        if (set.contains(folder + "/" + xjmFile.getName() + "/")) {
                            continue;
                        }
                        set.add(folder + "/" + xjmFile.getName() + "/");
                        zos.putNextEntry(new ZipEntry(folder + "/" + xjmFile.getName() + "/"));
                        zos.closeEntry();
                        zipFile(list, zos, userId, folder + "/" + xjmFile.getName(), set);
                    } else {
                        if (set.contains(xjmFile.getName() + "/")) {
                            continue;
                        }
                        set.add(xjmFile.getName() + "/");
                        zos.putNextEntry(new ZipEntry(xjmFile.getName() + "/"));
                        zos.closeEntry();
                        zipFile(list, zos, userId, xjmFile.getName(), set);
                    }


                }

            }


        } catch (Exception e) {
            log.error("文件打包失败", e);
        } finally {


        }
    }

    /**
     * 新建文件夹
     *
     * @param vo 新建文件夹信息
     * @return 新建文件夹结果
     */
    @Override
    public R newFolder(NewFolderReqVo vo) {
        // 获取用户id
        Long userId = SecurityUtils.getUserId();
        // 查询文件夹是否已存在
        XjmFile x = baseMapper.selectFileName(vo.getFolderName(), vo.getParentId(), userId);
        if (x != null) {
            return R.error(HttpStatus.SHOW_TOAST, "文件夹已存在");
        }
        // 创建文件夹
        XjmFile xjmFile = new XjmFile();
        xjmFile.setIsFolder(CommonCodeEnum.zero.getCode());
        xjmFile.setUserId(SecurityUtils.getUserId());
        xjmFile.setName(vo.getFolderName());
        xjmFile.setUserId(userId);
        xjmFile.setType(FileTypeEnum.FOLDER.getValue());
        xjmFile.setParentId(vo.getParentId());
        if ("0".equals(vo.getParentId().toString())) {
            xjmFile.setLevel("1");
            xjmFile.setTopId(0L);
        } else {
            XjmFile file = baseMapper.selectOne(new QueryWrapper<XjmFile>().lambda().eq(XjmFile::getId, vo.getParentId()));
            xjmFile.setLevel((Integer.parseInt(file.getLevel()) + 1) + "");
            if ("1".equals(file.getLevel())) {
                xjmFile.setTopId(file.getId());
            } else {
                xjmFile.setTopId(file.getTopId());
            }
        }
        baseMapper.insert(xjmFile);
        return R.success();
    }

    /**
     * 上传文件
     *
     * @param vo 上传文件信息
     * @return 上传文件结果
     */
    @Override
    public R uploadFile(UploadFileReqVo vo) {
        // 获取用户id
        Long userId = SecurityUtils.getUserId();
        // 判断文件是否已存在
        FileOutputStream outputStream = null;
        try {
            String hashCode = Hashing.md5().hashBytes(vo.getFile().getBytes()).toString().toString();
            // 比较md5值判断文件是否完整，不一致直接失败
            if (!hashCode.equals(vo.getChunkFileMd5())) {
                throw new RuntimeException("文件上传失败");
            }
            // 创建文件夹并返回输出流
            String folderPath = mPath + Constants.SEPARATOR + vo.getFileMd5() + Constants.SEPARATOR + userId;
            // 创建文件夹
            File file = new File(folderPath);
            boolean b = file.mkdirs();
            // 获取文件名
            String prefix = CommonUtils.getFileNamePrefix(vo.getFileName());
            String fileName = prefix + "_" + vo.getChunkNumber();
            // 创建文件
            File outFile = new File(folderPath + Constants.SEPARATOR + fileName);
            // 判断文件是否已存在，存在则不需要存储
            if (outFile.exists()) {
                return R.success();
            }
            // 创建一个输出流
            outputStream = new FileOutputStream(outFile);
            // 写入上传文件到服务器
            outputStream.write(vo.getFile().getBytes());
            outputStream.close();
            log.info("文件上传成功{}", vo.getChunkNumber());
        } catch (Exception e) {
            log.error("文件上传失败", e);
            throw new RuntimeException("文件上传失败");
        }
        return R.success();
    }

    /**
     * 合并文件
     *
     * @param vo 需要合并的文件信息
     * @return 合并结果
     */
    @Override
    public R merge(MergeFileReqVo vo) {
        Long userId = SecurityUtils.getUserId();
        // 获取文件类型
        String type = CommonUtils.getFileType(vo.getFileName());
        // 获取随机文件名称
        String s = UUID.generateRandomChars(5);
        // 拼接合并后的文件地址
        String savePath = vo.getFileMd5() + Constants.SEPARATOR + s;
        String path = mPath + Constants.SEPARATOR + savePath;
        // 创建静态文件路径
        String sPath = filePath + Constants.SEPARATOR + vo.getFileMd5() + Constants.SEPARATOR + s;
        // 文件后缀
        String fileNameSuffix = CommonUtils.getFileNameSuffix(vo.getFileName());
        // 创建合并后的文件
        String fileName = vo.getFileMd5() + fileNameSuffix;
        String targetFilePath = path + Constants.SEPARATOR + fileName;
        // 获取文件名称
        String filename = CommonUtils.getFileNamePrefix(vo.getFileName());
        // 直接插入文件信息
        XjmFile xjmFile = new XjmFile();
        xjmFile.setType(type);
        xjmFile.setUserId(userId);
        xjmFile.setName(vo.getFileName());
        xjmFile.setParentId(vo.getParentId());
        xjmFile.setMd5(vo.getFileMd5());
        xjmFile.setSize(vo.getSize());
        xjmFile.setStatus(FileStatusEnum.TRANSCODING.getCode());
        xjmFile.setPath(savePath + Constants.SEPARATOR + fileName);
        if ("0".equals(vo.getParentId().toString())) {
            xjmFile.setLevel("1");
            xjmFile.setTopId(0L);
        } else {
            XjmFile file = baseMapper.selectOne(new QueryWrapper<XjmFile>().lambda().eq(XjmFile::getId, vo.getParentId()));
            xjmFile.setLevel((Integer.parseInt(file.getLevel()) + 1) + "");
            if ("1".equals(file.getLevel())) {
                xjmFile.setTopId(file.getId());
            } else {
                xjmFile.setTopId(file.getTopId());
            }
        }
        baseMapper.insert(xjmFile);
        LoginUser loginUser = SecurityUtils.getLoginUser();
        XjmUser xjmUser = loginUser.getXjmUser();
        xjmUser.setSurplusCloudSize(xjmUser.getSurplusCloudSize() - vo.getSize());
        xjmUserService.updateById(xjmUser);
        loginUser.setXjmUser(xjmUser);
        tokenService.setLoginUser(loginUser);
        // 异步合并文件
        Runnable task = () -> {
            File file = new File(path);
            boolean b = file.mkdirs();
            // 合并文件
            for (int i = 0; i < vo.getTotalChunk(); i++) {
                // 拼接分片文件地址
                File partFile = new File(mPath + Constants.SEPARATOR + vo.getFileMd5() + Constants.SEPARATOR + userId + Constants.SEPARATOR + filename + "_" + i);
                FileInputStream fileInputStream = null;
                try (FileOutputStream fos = new FileOutputStream(targetFilePath, true)) {
                    byte[] buffer = new byte[1024];
                    int bytesRead;
                    // 读取分片文件内容并写入合并后的文件
                    fileInputStream = new FileInputStream(partFile);
                    while ((bytesRead = fileInputStream.read(buffer)) != -1) {
                        fos.write(buffer, 0, bytesRead);
                    }
                    log.info("文件合并成功" + i);
                    // 删除分片文件
                    fileInputStream.close();
                    fos.close();
                    boolean delete = partFile.delete();
                    log.info("文件删除成功" + delete);
                } catch (Exception e) {
                    log.error("合并文件失败", e);
                }
            }

            // 判断是否是视频文件是则切片获取封面图
            String status = chunkVideo(path, sPath, targetFilePath, type, vo.getFileMd5(), fileNameSuffix);

            //判断是否为图片，是则生成缩略图
            compressionImage(type, sPath, targetFilePath, fileName, vo.getFileName());

            // 合并完成更新文件信息
            boolean delete = new File(mPath + Constants.SEPARATOR + vo.getFileMd5() + Constants.SEPARATOR + userId).delete();
            XjmFile updateFile = new XjmFile();
            updateFile.setId(xjmFile.getId());
            updateFile.setStatus(status);
            updateFile.setUpdateTime(new Date());
            baseMapper.updateById(updateFile);



        };
        taskExecutor.execute(task);
        return R.success();
    }

    /**
     * 检查是否有该文件
     *
     * @param vo 文件信息
     * @return 检查结果
     */
    @Override
    public R check(FileCheckReqVo vo) {
        Long userId = SecurityUtils.getUserId();
        // 查询文件夹是否已存在
        List<XjmFile> xjmFileList = baseMapper.selectList(new QueryWrapper<XjmFile>().lambda().eq(XjmFile::getMd5, vo.getMd5()));
        // 检查空间是否足够
        XjmUser xjmUser = SecurityUtils.getLoginUser().getXjmUser();
        if (xjmUser.getSurplusCloudSize() - vo.getFileSize() < 0) {
            return R.error(HttpStatus.SHOW_TOAST, "空间不足");
        }
        if (xjmFileList != null && !xjmFileList.isEmpty()) {
            // 存在直接写入当前用户文件夹
            XjmFile xjmFile = xjmFileList.get(0);
            xjmFile.setId(null);
            xjmFile.setName(vo.getFileName());
            xjmFile.setParentId(vo.getParentId());
            xjmFile.setUserId(userId);
            baseMapper.insert(xjmFile);
            return R.success().put("exist", true);
        } else {
            // 不存在则判断当前用户是否上传过分片
            File file = new File(filePath + Constants.SEPARATOR + vo.getMd5() + Constants.SEPARATOR + userId);
            File[] files = null;
            if (file.exists() && Objects.requireNonNull(files = file.listFiles()).length != 0) {
                // 存在则返回最小需要上传的分片
                int index = 0;
                ArrayList<Integer> indexes = new ArrayList<>();
                for (File itemFile : files) {
                    int current = Integer.parseInt(itemFile.getName().substring(itemFile.getName().lastIndexOf("_") + 1));
                    indexes.add(current);
                }
                for (int i = 0; i < vo.getTotalNum(); i++) {
                    boolean contains = indexes.contains(i);
                    if (!contains) {
                        index = i;
                        break;
                    }
                }
                return Objects.requireNonNull(R.success().put("exist", false)).put("index", index);
            }
            return R.success().put("exist", false);
        }
    }

    /**
     * 上传文件
     *
     * @param vo 上传文件信息
     * @return 上传文件结果
     */
    @Override
    @Transactional
    public R upload(UploadReqVo vo) {
        // 判断文件大小更新用户云盘大小
        XjmUser xjmUser = SecurityUtils.getLoginUser().getXjmUser();
        if (xjmUser.getSurplusCloudSize() - vo.getFileSize() < 0) {
            return R.error(HttpStatus.SHOW_TOAST, "空间不足");
        }
        xjmUser.setSurplusCloudSize(xjmUser.getSurplusCloudSize() - vo.getFileSize());
        xjmUserService.updateById(xjmUser);
        LoginUser loginUser = SecurityUtils.getLoginUser();
        loginUser.setXjmUser(xjmUser);
        tokenService.setLoginUser(loginUser);
        // 获取随机文件名称
        String s = UUID.generateRandomChars(5);
        // 保存路径
        String savePath = vo.getFileMd5() + Constants.SEPARATOR + s;
        // 拼接合并后的文件地址
        String path = mPath + Constants.SEPARATOR + savePath;
        // 拼接静态资源路径
        String sPath = filePath + Constants.SEPARATOR + vo.getFileMd5() + Constants.SEPARATOR + s;
        // 获取文件类型
        String type = CommonUtils.getFileType(vo.getFileName());
        Long userId = SecurityUtils.getUserId();
        File targetFile = new File(path);
        boolean b = targetFile.mkdirs();
        // 文件后缀
        String fileNameSuffix = CommonUtils.getFileNameSuffix(vo.getFileName());
        String fileName = vo.getFileMd5() + fileNameSuffix;
        String targetFilePath = path + Constants.SEPARATOR + fileName;
        try (FileOutputStream fos = new FileOutputStream(targetFilePath)) {
            fos.write(vo.getFile().getBytes());
            // 直接插入文件信息
            XjmFile xjmFile = new XjmFile();
            xjmFile.setType(type);
            xjmFile.setUserId(userId);
            xjmFile.setName(vo.getFileName());
            xjmFile.setParentId(vo.getParentId());
            xjmFile.setMd5(vo.getFileMd5());
            xjmFile.setSize(vo.getFile().getSize());
            xjmFile.setPath(savePath + Constants.SEPARATOR + fileName);
            xjmFile.setUserId(userId);
            xjmFile.setMd5(vo.getFileMd5());
            xjmFile.setStatus(FileStatusEnum.NORMAL.getCode());
            if ("0".equals(vo.getParentId().toString())) {
                xjmFile.setLevel("1");
                xjmFile.setTopId(0L);
            } else {
                XjmFile file = baseMapper.selectOne(new QueryWrapper<XjmFile>().lambda().eq(XjmFile::getId, vo.getParentId()));
                xjmFile.setLevel((Integer.parseInt(file.getLevel()) + 1) + "");
                if ("1".equals(file.getLevel())) {
                    xjmFile.setTopId(file.getId());
                } else {
                    xjmFile.setTopId(file.getTopId());
                }
            }
            baseMapper.insert(xjmFile);
            // 异步执行耗时操作
            Runnable task = () -> {
                // 判断是否是视频文件是则切片获取封面图
                String status = chunkVideo(path, sPath, targetFilePath, type, vo.getFileMd5(), fileNameSuffix);
                // 判断是否为图片，是则生成缩略图
                compressionImage(type, sPath, targetFilePath, fileName, vo.getFileName());
                // 更新文件信息
                XjmFile updatexjmFile = new XjmFile();
                updatexjmFile.setUserId(userId);
                updatexjmFile.setStatus(status);
                updatexjmFile.setMd5(vo.getFileMd5());
                baseMapper.updateStatus(xjmFile);
            };
            taskExecutor.execute(task);
            return R.success();
        } catch (Exception e) {
            return R.error();
        }
    }

    /**
     * 预览图片
     *
     * @param id       文件id
     * @param response 响应体
     */
    @Override
    public void previewImage(String id, HttpServletResponse response) {
        XjmFile xjmFile = getFile(id, response);
        if (xjmFile == null) return;
        // 不是图片类型直接返回
        if (!xjmFile.getType().equals(FileTypeEnum.IMAGE.getValue())) {
            response.setStatus(HttpStatus.ERROR);
            return;
        }
        String path = mPath + Constants.SEPARATOR + xjmFile.getPath();
        String name = xjmFile.getName();
        String fileNameSuffixNo = CommonUtils.getFileNameSuffixNo(name);
        response.setContentType("image/" + fileNameSuffixNo);
        response.setHeader("Cache-Control", "max-age=2592000");
        try {
            readFile(path, response);
        } catch (Exception e) {
            log.error("预览文件失败", e);
        }
    }

    /**
     * 预览视频
     *
     * @param id       文件id
     * @param fileName 文件名称
     * @param response 响应体
     */
    @Override
    public void previewVideo(String id, String fileName, HttpServletResponse response) {
        XjmFile xjmFile = getFile(id, response);
        if (xjmFile == null) return;
        // 不是视频类型直接返回
        if (!xjmFile.getType().equals(FileTypeEnum.VIDEO.getValue())) {
            response.setStatus(HttpStatus.ERROR);
            return;
        }
        String path = mPath + Constants.SEPARATOR + xjmFile.getPath();
        String filePath = path.substring(0, path.lastIndexOf("."));
        String file = null;
        if (fileName.endsWith(".ts")) {
            file = filePath + Constants.SEPARATOR + fileName;
            if (!new File(file).exists()) return;
        } else {
            file = filePath + Constants.SEPARATOR + xjmFile.getMd5() + ".m3u8";
            if (!new File(file).exists()) return;
        }
        try {
            response.setHeader("Cache-Control", "max-age=2592000");
            readFile(file, response);
        } catch (Exception e) {
            log.error("预览文件失败", e);
        }
    }

    private XjmFile getFile(String id, HttpServletResponse response) {
        XjmFile xjmFile = redisCache.getPaserCacheObject(CacheConstants.FILE_CACHE_PREFIX + id, XjmFile.class);
        if (xjmFile == null) {
            xjmFile = baseMapper.selectById(id);
            redisCache.setCacheObject(CacheConstants.FILE_CACHE_PREFIX + id, xjmFile);
            redisCache.expire(CacheConstants.FILE_CACHE_PREFIX + id, 1, TimeUnit.HOURS);
        }

        if (xjmFile == null || StringUtils.isEmpty(xjmFile.getPath())) {
            response.setStatus(HttpStatus.ERROR);
            return null;
        } else if (xjmFile.getStatus().equals(FileStatusEnum.TRANSCODING.getCode())) {
            response.setStatus(HttpStatus.ERROR);
            return null;
        } else if (xjmFile.getStatus().equals(FileStatusEnum.TRANSCODING_FAILURE.getCode()) || xjmFile.getStatus().equals(FileStatusEnum.RECYCLING.getCode()) || xjmFile.getStatus().equals(FileStatusEnum.DELETE.getCode())) {
            response.setStatus(HttpStatus.ERROR);
            return null;
        }
        return xjmFile;
    }

    private void compressionImage(String type, String path, String targetFilePath, String name, String fileName) {
        if (type.equals(FileTypeEnum.IMAGE.getValue())) {
            File file = new File(targetFilePath);
            if (!file.exists()) {
                return;
            }
            String outputDirectory = path + Constants.SEPARATOR + name + Constants.THUMB_IMG_SUFFIX;
            try {
                Path p = Paths.get(path);
                Files.createDirectories(p);
                FileInputStream fileInputStream = new FileInputStream(file);
                if (".gif".equals(CommonUtils.getFileNameSuffix(fileName))) {
                    String command = String.format("ffmpeg -i %s -vf \"thumbnail=200,scale=300:-1\" -frames:v 1 %s", targetFilePath, outputDirectory);

                    // 构建进程
                    int exitCode = execCmd(command);
                    if (exitCode != 0) {
                        // 转码失败，处理逻辑
                        log.error("生成缩略图失败");
                    }
                } else if (".webp".equals(CommonUtils.getFileNameSuffix(fileName))) {

                } else {
                    Thumbnails.of(fileInputStream).scale(0.25).outputFormat("jpg").imageType(ThumbnailParameter.DEFAULT_IMAGE_TYPE).outputQuality(0.25f).toFile(outputDirectory);
                }
            } catch (Exception e) {
                log.error("生成缩略图失败", e);
            }
        }
    }

    private String chunkVideo(String path, String sPath, String targetFilePath, String type, String fileMd5, String fileNameSuffix) {
        if (type.equals(FileTypeEnum.VIDEO.getValue())) {
            // 获取视频封面图片
            getVideoImage(sPath, fileMd5, targetFilePath, fileNameSuffix);
            // 如果为视频文件则切片转码
            try {
                String outputDirectory = path + Constants.SEPARATOR + fileMd5;
                File outputDirectoryfile = new File(outputDirectory);
                if (!outputDirectoryfile.exists()) {
                    outputDirectoryfile.mkdir();
                }
                // 判断文件类型是否需要转码，否则直接切片（未知格式或发生异常默认不转码）
                String command = "";
                String[] inputFileFormat = getInputFileFormat(targetFilePath);
                List<String> noFormatCoding = Arrays.asList("acc", "h264", "h265", "VP9", "ProRes");
                List<String> noFormatType = Arrays.asList("mp4", "webm", "mov");
                boolean isFormat = false;
                if (inputFileFormat[1] != null) {
                    String[] split = inputFileFormat[1].split(",");
                    for (String s : split) {
                        if (noFormatType.contains(s)) {
                            isFormat = true;
                            break;
                        }
                    }
                }
                if (inputFileFormat[0] == null || (noFormatCoding.contains(inputFileFormat[0]) && isFormat)) {
                    command = String.format("ffmpeg -i %s -c copy -f hls -hls_list_size 0 -segment_list playlist.m3u8 -segment_list_flags 0 -hls_time 15 -y %s/%s.m3u8", targetFilePath, outputDirectory, fileMd5);
                } else {
                    command = String.format("ffmpeg -i %s -c:v libx264 -crf 23 -c:a copy -b:a 128k -f hls -hls_list_size 0 -segment_list playlist.m3u8 -segment_list_flags 0 -hls_time 15 -y %s/%s.m3u8", targetFilePath, outputDirectory, fileMd5);
                }
                // 构建进程
                int exitCode = execCmd(command);

                if (exitCode != 0) {
                    // 转码失败，处理逻辑
                    log.error("转码失败");
                    return FileStatusEnum.TRANSCODING_FAILURE.getCode();
                }
            } catch (Exception e) {
                log.error("转码失败");
                return FileStatusEnum.TRANSCODING_FAILURE.getCode();
            }
        }
        return FileStatusEnum.NORMAL.getCode();
    }

    private int execCmd(String command) throws IOException, InterruptedException {
        Process process = Runtime.getRuntime().exec(command.split(" "));

        // 读取进程标准输出
        new Thread(() -> {
            try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
                String line = null;
                while ((line = bufferedReader.readLine()) != null) {
                    log.info(line);
                }
            } catch (IOException e) {
            }
        }).start();

        // 读取进程异常输出
        new Thread(() -> {
            try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
                String line = null;
                while ((line = bufferedReader.readLine()) != null) {
                    log.info(line);
                }
            } catch (IOException e) {
            }
        }).start();

        int exitCode = process.waitFor();
        return exitCode;
    }


    /**
     * 判断视频文件格式
     *
     * @param filePath 文件路径
     * @return 文件格式
     */
    private static String[] getInputFileFormat(String filePath) {
        String[] command = {"ffprobe", "-v", "error", "-show_format", "-show_streams", filePath};

        try {
            final Process process = new ProcessBuilder(command).start();
            BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
            List<String> outputLines = new ArrayList<>();
            String line;
            while ((line = reader.readLine()) != null) {
                outputLines.add(line);
            }

            System.out.println("FFprobe output: " + outputLines);

            String codecName = null;
            String containerFormat = null;

            for (String output : outputLines) {
                if (output.contains("codec_name=") && codecName == null) {
                    codecName = output.substring(output.indexOf("=") + 1);
                }
                if (output.contains("format_name=") && containerFormat == null) {
                    containerFormat = output.substring(output.indexOf("=") + 1);
                }
            }

            System.out.println("Detected codec: " + codecName);
            System.out.println("Detected container: " + containerFormat);

            return new String[]{codecName, containerFormat};
        } catch (IOException e) {
            System.err.println("Error getting input file format: " + e.getMessage());
            throw new RuntimeException("Error getting input file format", e);
        }
    }

    public void getVideoImage(String path, String md5, String targetFilePath, String fileNameSuffix) {
        try {
            Path p = Paths.get(path);
            Files.createDirectories(p);
            String outPath = path + Constants.SEPARATOR + md5 + fileNameSuffix + Constants.THUMB_IMG_SUFFIX;
            log.info("开始获取视频封面图");
            List<String> commands = new ArrayList<>();
            commands.add("ffmpeg");
            commands.add("-i");
            commands.add(targetFilePath);
            commands.add("-ss");
            commands.add("00:00:01");
            commands.add("-y");
            commands.add("-q:v");
            commands.add("1");
            commands.add("-frames:v");
            commands.add("1");
            commands.add("-f");
            commands.add("image2");
            commands.add(outPath);

            Process jpgProcess = new ProcessBuilder(commands).start();

            // 读取进程标准输出
            new Thread(() -> {
                try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(jpgProcess.getInputStream()))) {
                    String line = null;
                    while ((line = bufferedReader.readLine()) != null) {
                        log.info(line);
                    }
                } catch (IOException e) {
                }
            }).start();

            // 读取进程异常输出
            new Thread(() -> {
                try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(jpgProcess.getErrorStream()))) {
                    String line = null;
                    while ((line = bufferedReader.readLine()) != null) {
                        log.error(line);
                    }
                } catch (IOException e) {
                }
            }).start();

            jpgProcess.waitFor();
            log.info("获取视频封面图成功");
        } catch (Exception e) {
            log.error("获取视频封面图失败", e);
            throw new RuntimeException(e);
        }
    }

    public List<FileListRespVo> getTreeByFileIds(List<Long> ids) {
        List<XjmFile> xjmFiles = baseMapper.selectList(new QueryWrapper<XjmFile>().lambda().in(XjmFile::getId, ids).eq(XjmFile::getStatus, FileStatusEnum.NORMAL.getCode()));
        // 先按日期排序在按status为0的在前面
        List<XjmFile> files = xjmFiles.stream().sorted(Comparator.comparing(XjmFile::getCreateTime).reversed()).sorted(Comparator.comparing(XjmFile::getIsFolder)).collect(Collectors.toList());
        // 转换为返回vo
        return transformTree(files, ip, port);
    }
}

